import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

public class Router {

	private int id, time;

	private long lastTime;

	private Map<Integer, Long> timestamp;

	private Map<Integer, Entry> table;

	private Map<Integer, Vector<Entry>> received;

	private Map<Integer, Neighbour> neighbours;

	private DatagramSocket socket;

	/**
	 * Cria um roteador, com sua configuracao inicial (identificador, porta, e
	 * tempo para indicar a morte dos vizinhos).
	 *
	 * @param id
	 *            identificador deste roteador.
	 * @param port
	 *            porta que este roteador escutara.
	 * @param time
	 *            tempo para indicar morte dos vizinhos.
	 * @throws SocketException
	 *             caso ele nao consiga criar o Socket
	 */
	public Router(int id, int port, int time, Vector<Neighbour> neighbours)
			throws SocketException {
		this.id = id;
		this.time = time;

		createMaps();
		createSocket(port);
		initializeNeighbours(neighbours);
		initializeAlert();

	}

	private void initializeNeighbours(Vector<Neighbour> vetor) {
		for (Neighbour n : vetor) {
			int id = n.getId();

			this.neighbours.put(id, n);
			this.timestamp.put(id, timeStamp());
			this.table.put(id, new Entry(id, n.getCost(), id));
		}
	}

	private void createSocket(int port) throws SocketException {
		this.socket = new DatagramSocket(port);
	}

	private void createMaps() {
		this.timestamp = new HashMap<Integer, Long>();
		this.table = new HashMap<Integer, Entry>();
		this.received = new HashMap<Integer, Vector<Entry>>();
		this.neighbours = new HashMap<Integer, Neighbour>();
	}

	/**
	 * Cria um alerta, para mostrar que o roteador esta vivo (evitar timeout), e
	 * enviar a sua tabela periodicamente.
	 */
	private void initializeAlert() {
		Timer t = new Timer(true);

		t.schedule(new TimerTask() {
			@Override
			public void run() {
				if (timeStamp() - lastTime >= time) {
					remakeTable();
					sendMessageAllNeighbours();
				}
			}
		}, this.time, this.time);
	}

	private void waitIncomeMessages() throws Exception {
		byte[] receiveData = new byte[1024];

		while (true) {
			DatagramPacket receivePacket = new DatagramPacket(receiveData,
					receiveData.length);
			socket.receive(receivePacket);

			String message = new String(receivePacket.getData()).trim();

			System.out.println("Recebi <" + message + ">");

			String[] split = message.split(":");

			Integer from = Integer.parseInt(split[0]);

			timestamp.put(from, timeStamp());

			Vector<Entry> vector = createVector(from, split[1]);

			this.received.put(from, vector);

			if (remakeTable()) {
				sendMessageAllNeighbours();
			}
		}
	}

	private long timeStamp() {
		return System.currentTimeMillis();
	}

	private boolean remakeTable() {
		boolean result = false;

		// cria uma nova tabela, com a equacao de Bellman-Ford e os vizinhos
		Map<Integer, Entry> newTable = createTable();

		System.out.println("Nova tabela: " + newTable);
		System.out.println("Antiga tabela: " + this.table);
		// faz a intersecao entre as entradas novas e as antigas (elementos
		// iguais)
		Set<Integer> newInterOld = new HashSet<Integer>(newTable.keySet());
		newInterOld.retainAll(this.table.keySet());

		for (Integer key : newInterOld) {
			Entry old = this.table.get(key);
			Entry min = old.min(newTable.get(key));

			result |= !(old.equals(min));

			this.table.put(key, min);
		}

		// faz a new - old (elementos novos)
		Set<Integer> newDiffOld = new HashSet<Integer>(newTable.keySet());
		newDiffOld.removeAll(this.table.keySet());

		result |= newDiffOld.size() > 0;

		for (Integer key : newDiffOld) {
			System.out.println(key + " eh novo.");
			this.table.put(key, newTable.get(key));
		}

		// faz a old - new (elementos removidos)
		Set<Integer> oldDiffNew = new HashSet<Integer>(this.table.keySet());
		oldDiffNew.removeAll(newTable.keySet());

		// TODO talvez remover
		for (Integer key : oldDiffNew) {
			/* this.table.remove(key); */
			if (this.neighbours.containsKey(key)
					&& timeStamp() - this.timestamp.get(key) < 3 * time)
				continue;

			result |= this.table.containsKey(key)
					&& this.table.get(key).getCost() < Entry.INFINITO;
			this.table.put(key, new Entry(key, Entry.INFINITO, key));
		}

		return result;
	}

	private Map<Integer, Entry> createTable() {
		Map<Integer, Entry> result = new HashMap<Integer, Entry>();

		for (Integer key : received.keySet()) {

			if (timeStamp() - timestamp.get(key) < 3 * this.time) {
				System.out.println("TIME = " + this.time);
				System.out.println("TIMESTAMP(Key) = " + this.timestamp.get(key));
				System.out.println("TIMESTAMP() = " + this.timeStamp());

				for (Entry entry : received.get(key)) {
					int id = entry.getId();

					int cost = Math.min(entry.getCost()
							+ this.neighbours.get(key).getCost(),
							Entry.INFINITO);

					Entry add = new Entry(id, cost, key);

					if (result.containsKey(id))
						add = add.min(result.get(id));

					result.put(id, add);
				}
			}
		}
		return result;
	}

	private Vector<Entry> createVector(int next_hop, String vetor) {
		Vector<Entry> result = new Vector<Entry>();

		if (vetor.length() > 0)
			for (String entry : vetor.split(";"))
				result.add(new Entry(entry, next_hop));

		return result;
	}

	private void sendMessageAllNeighbours() {
		for (Integer i : this.neighbours.keySet()) {
			sendMessageTo(i);
		}

		lastTime = timeStamp();
	}

	private void sendMessageTo(Integer to) {
		Neighbour n = this.neighbours.get(to);

		try {
			String message = createMessage(to);

			System.out.println("Enviando para " + to + ": " + message);

			byte[] sendData = message.getBytes();
			DatagramPacket p = new DatagramPacket(sendData, sendData.length, n
					.getIp(), n.getPort());

			socket.send(p);
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	private String createMessage(Integer to) {
		String message = String.valueOf(this.id) + ":";

		Vector<Entry> entries = new Vector<Entry>(this.table.values());

		if (entries.size() > 0)
			message += entries.get(0).getAsString(to);

		for (int i = 1; i < entries.size(); i++)
			message += ";" + entries.get(i).getAsString(to);

		return message;
	}

	public static void main(String[] args) throws Exception {

		if (args.length != 2) {

			System.out.println(args.length);
			System.out.println("USAGE: java Router <id> <time>");
			System.exit(1);
		}

		int id = Integer.parseInt(args[0]);
		int time = Integer.parseInt(args[1]);
		int port = 0;

		Map<Integer, Neighbour> tmp = new HashMap<Integer, Neighbour>();

		Scanner routerCfg = new Scanner(new FileInputStream(new File(
				"roteador.config")));
		while (routerCfg.hasNextLine()) {
			String line = routerCfg.nextLine().trim();

			String[] parts = line.split(" ");

			int tID = Integer.parseInt(parts[0]);
			int tPORT = Integer.parseInt(parts[1]);
			String tADDRESS = parts[2];

			if (tID == id) {
				port = tPORT;
			} else {
				tmp.put(tID,
						new Neighbour(tID, Entry.INFINITO, tPORT, tADDRESS));
			}
		}
		routerCfg.close();

		Scanner linkCfg = new Scanner(new FileInputStream(new File(
				"enlaces.config")));

		Vector<Neighbour> links = new Vector<Neighbour>();
		while (linkCfg.hasNextLine()) {
			String line = linkCfg.nextLine().trim();

			String[] parts = line.split(" ");

			int idA = Integer.parseInt(parts[0]);
			int idB = Integer.parseInt(parts[1]);
			int cost = Integer.parseInt(parts[2]);

			if (idA == id) {
				Neighbour tN = tmp.get(idB);
				links.add(new Neighbour(idB, cost, tN.getPort(), tN.getIp()));
			} else if (idB == id) {
				Neighbour tN = tmp.get(idA);
				links.add(new Neighbour(idA, cost, tN.getPort(), tN.getIp()));
			}
		}
		linkCfg.close();

		Router r = new Router(id, port, time, links);
		r.waitIncomeMessages();

	}

}
